/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */


package org.apache.skywalking.apm.plugin.tomcat78x;

import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.skywalking.apm.agent.core.context.CarrierItem;
import org.apache.skywalking.apm.agent.core.context.ContextCarrier;
import org.apache.skywalking.apm.agent.core.context.ContextManager;
import org.apache.skywalking.apm.agent.core.context.tag.Tags;
import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
import org.apache.skywalking.apm.agent.core.context.trace.TraceSegment;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
import org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;

/**
 * {@link TomcatInvokeInterceptor} fetch the serialized context data by using {@link
 * HttpServletRequest#getHeader(String)}. The {@link TraceSegment#refs} of current trace segment will reference to the
 * trace segment id of the previous level if the serialized context is not null.
 */
// InstMethodsInter 实现类会在目标方法前后调用相关的方法
public class TomcatInvokeInterceptor implements InstanceMethodsAroundInterceptor {

    /**
     * * The {@link TraceSegment#refs} of current trace segment will reference to the
     * trace segment id of the previous level if the serialized context is not null.
     *
     * @param objInst
     * @param method
     * @param allArguments
     * @param argumentsTypes
     * @param result         change this result, if you want to truncate the method.
     * @throws Throwable
     */
    // 三种场景：
    //
    // 场景一：Tomcat 作为用户请求接入层的场景。
    //        此时请求没有与任何 Trace 关联，也就不会携带 ContextCarrier 请求头，beforeMethod()
    //        方法中会创建全新的 TracingContext 以及 EntrySpan。
    //
    // 场景二：tomcat-7.x-8.x-plugin 插件被嵌套在其他插件之后的场景。
    //        此时请求在经过其他插件的时候，已经创建了关联的 TracingContext 以及 EntrySpan，beforeMethod()
    //        方法无需创建 TracingContext，只需重新调用 EntrySpan 的 start() 方法即可。
    //
    // 场景三：Tomcat 作为下游系统被其他系统调用的场景。
    //        此时请求已经在上游系统中关联了 Trace，在跨进程 HTTP 调用时就会携带 ContextCarrier
    //        请求头，在 TomcatInstrumentation 的 beforeMethod() 方法中进行反序列化，并填充到
    //        全新的 TracingContext 中，还会新建 EntrySpan 并调用其 start() 方法。
    @Override
    public void beforeMethod(EnhancedInstance objInst,
                             Method method,
                             Object[] allArguments,
                             Class<?>[] argumentsTypes,
                             MethodInterceptResult result) throws Throwable {

        // invoke()方法的第一个参数就是HttpServletRequest对象
        HttpServletRequest request = (HttpServletRequest) allArguments[0];
        // 创建一个空的ContextCarrier对象
        ContextCarrier contextCarrier = new ContextCarrier();

        // 创建CarrierItem链表，因为ContextCarrier对象是空的，所以链表也是空的
        CarrierItem next = contextCarrier.items();
        while (next.hasNext()) {
            next = next.next();
            // 拿到HttpHeader的Value，即对应版本的ContextCarrier序列化字符串
            next.setHeadValue(request.getHeader(next.getHeadKey()));
        }

        // 获取当前线程绑定的TracingContext，如果未绑定则会创建新TracingContext并
        // 绑定，同时还会创建EntrySpan，如果已存在EntrySpan，则再次调用其start()方
        // 法。这里的第一个参数是operationName(即EndpointName)，Tomcat的场景下就
        // 是请求的 URI。
        AbstractSpan span = ContextManager.createEntrySpan(request.getRequestURI(), contextCarrier);
        // 为EntrySpan添加Tags，记录请求的URL以及Method信息
        Tags.URL.set(span, request.getRequestURL().toString());
        Tags.HTTP.METHOD.set(span, request.getMethod());
        // 设置component字段
        span.setComponent(ComponentsDefine.TOMCAT);
        // 设置layer字段
        SpanLayer.asHttp(span);

    }

    /**
     * 请求经过 beforeMethod() 方法处理之后，会继续调用 StandardHostValve.invoke() 这个目标方法。 在 invoke()
     * 方法返回之后，继续执行 TomcatInvokeInterceptor.afterMethod() 的后置处理，请求会调用当前 stopSpan() 关闭
     * 当前 Span（即前面创建的 EntrySpan），同时会根据 HTTP 响应码在 Span 中标记该请求是否发生异常，记录相关 Tags
     * 信息等。
     */
    @Override
    public Object afterMethod(EnhancedInstance objInst,
                              Method method,
                              Object[] allArguments,
                              Class<?>[] argumentsTypes,
                              Object ret) throws Throwable {
        // invoke()方法的第二个参数是 HttpServletResponse
        HttpServletResponse response = (HttpServletResponse) allArguments[1];

        // 获取当前Span，因为TracingContext是栈的形式管理Span，当前Span即为beforeMethod()方法中创建的EntrySpan
        AbstractSpan span = ContextManager.activeSpan();
        if (response.getStatus() >= 400) {
            // 如果响应码是4xx或是5xx，则表示Http响应异常，标记当前Span的errorOccurred字段，并记录一个Key为status_code的Tag
            span.errorOccurred();
            Tags.STATUS_CODE.set(span, Integer.toString(response.getStatus()));
        }
        // 关闭当前EntrySpan，如果EntrySpan完全关闭，则整个Span栈为空，所在的TraceSegment也将随之关闭
        ContextManager.stopSpan();
        // 从RuntimeContext中清理FORWARD_REQUEST_FLAG信息
        ContextManager.getRuntimeContext().remove(Constants.FORWARD_REQUEST_FLAG);
        return ret;
    }

    /**
     * 在 StandardHostValve.invoke() 方法处理请求抛出异常时，TomcatInvokeInterceptor.handleMethodException()
     * 方法会在当前 Span 中记录 Log 信息，并通过 Span 的 errorOccurred 字段标记该请求处理异常。
     */
    @Override
    public void handleMethodException(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes, Throwable t) {
        AbstractSpan span = ContextManager.activeSpan();
        span.log(t);
        span.errorOccurred();
    }
}
